
#pragma once

#include "EZValue.h"
#include <iomanip>
#include <sstream>
#include <string.h>

class EZWriter
{

public:
	std::string write(const EZValue& val);

private:
	void writeValue(int depth, const EZValue& val);

	std::string valueToQuotedString(const char *value);

	bool isControlCharacter(char ch) {
		return ch > 0 && ch <= 0x1F;
	}

	bool containsControlCharacter(const char* str)
	{
		while (*str)
		{
			if (isControlCharacter(*(str++))) {
				return true;
			}
		}
		return false;
	}
private:
	std::string jsonstr_;
};

inline std::string EZWriter::write(const EZValue& val)
{
	jsonstr_ = "";
	writeValue(0, val);
	return jsonstr_;
}

inline void EZWriter::writeValue(int depth, const EZValue& val)
{
	switch (val.type())
	{
	case EZValueType::nullValue:
		jsonstr_ += "null";
		break;
	case EZValueType::int64Value:
		jsonstr_ += std::to_string(val.asInt64());
		break;
	case EZValueType::uint64Value:
		jsonstr_ += std::to_string(val.asUInt64());
		break;
	case EZValueType::realValue:
		jsonstr_ += std::to_string(val.asDouble());
		break;
	case EZValueType::stringValue:
		jsonstr_ += valueToQuotedString(val.asString().c_str());
		break;
	case EZValueType::booleanValue:
		jsonstr_ += val.asBool() ? "true" : "false";
		break;
	case EZValueType::arrayValue:
	{
		size_t len = val.size();
		jsonstr_ += "[";
		for (size_t i = 0; i < len; ++i)
		{
			if (i > 0) {
				jsonstr_ += ",";
			}
			writeValue(depth, val[i]);
		}
		jsonstr_ += "]";
	}
		break;
	case EZValueType::objectValue:
	{
		auto mb = val.memberNames();
		jsonstr_ += "{";
		for (auto it = mb.begin(); it != mb.end(); ++it)
		{
			if (it != mb.begin()) {
				jsonstr_ += ",";
			}
			jsonstr_ += valueToQuotedString((*it).c_str());
			jsonstr_ += ":";
			writeValue(depth + 1, val[*it]);
		}
		jsonstr_ += "}";	
	}
		break;
	default:
		break;
	}
}


inline std::string EZWriter::valueToQuotedString(const char *value)
{
	// Not sure how to handle unicode...
	if (strpbrk(value, "\"\\\b\f\n\r\t") == NULL && !containsControlCharacter(value))
		return std::string("\"") + value + "\"";
	// We have to walk value and escape any special characters.
	// Appending to std::string is not efficient, but this should be rare.
	// (Note: forward slashes are *not* rare, but I am not escaping them.)
	size_t maxsize = strlen(value) * 2 + 3; // allescaped+quotes+NULL
	std::string result;
	result.reserve(maxsize); // to avoid lots of mallocs
	result += "\"";
	for (const char* c = value; *c != 0; ++c)
	{
		switch (*c)
		{
		case '\"':
			result += "\\\"";
			break;
		case '\\':
			result += "\\\\";
			break;
		case '\b':
			result += "\\b";
			break;
		case '\f':
			result += "\\f";
			break;
		case '\n':
			result += "\\n";
			break;
		case '\r':
			result += "\\r";
			break;
		case '\t':
			result += "\\t";
			break;
			//case '/':
			// Even though \/ is considered a legal escape in JSON, a bare
			// slash is also legal, so I see no reason to escape it.
			// (I hope I am not misunderstanding something.
			// blep notes: actually escaping \/ may be useful in javascript to avoid </ 
			// sequence.
			// Should add a flag to allow this compatibility mode and prevent this 
			// sequence from occurring.
		default:
			if (isControlCharacter(*c))
			{
				std::ostringstream oss;
				oss << "\\u" << std::hex << std::uppercase << std::setfill('0') << std::setw(4) << static_cast<int>(*c);
				result += oss.str();
			}
			else
			{
				result += *c;
			}
			break;
		}
	}
	result += "\"";
	return result;
}





class EZStyledWriter{
public:
  EZStyledWriter() : rightMargin_(74), indentSize_(3), addChildValues_() {}
  ~EZStyledWriter(){}

public: 
  /** \brief Serialize a Value in <a HREF="http://www.json.org">JSON</a> format.
   * \param root Value to serialize.
   * \return String containing the JSON document that represents the root value.
   */
  std::string write(const EZValue& root);

private:
  void writeValue(const EZValue& value);
  void writeArrayValue(const EZValue& value);
  bool isMultineArray(const EZValue& value);
  void pushValue(const std::string& value);
  void writeIndent();
  void writeWithIndent(const std::string& value);
  void indent();
  void unindent();
  std::string valueToQuotedString(const char *value);
  bool isControlCharacter(char ch);
  bool containsControlCharacter(const char* str);

  typedef std::vector<std::string> ChildValues;

  ChildValues childValues_;
  std::string document_;
  std::string indentString_;
  unsigned int rightMargin_;
  unsigned int indentSize_;
  bool addChildValues_;
};


inline std::string EZStyledWriter::write(const EZValue& root) 
{
	document_.clear();
	addChildValues_ = false;
	indentString_.clear();
	writeValue(root);
	document_ += "\n";
	return document_;
}

inline void EZStyledWriter::writeValue(const EZValue& value) 
{
	switch (value.type()) {
	case EZValueType::nullValue:
		pushValue("null");
		break;
	case EZValueType::int64Value:
		pushValue(std::to_string(value.asInt64()));
		break;
	case EZValueType::uint64Value:
		pushValue(std::to_string(value.asUInt64()));
		break;
	case EZValueType::realValue:
		pushValue(std::to_string(value.asDouble()));
		break;
	case EZValueType::stringValue:
		pushValue(valueToQuotedString(value.asString().c_str()));
		break;
	case EZValueType::booleanValue:
		pushValue(value.asBool() ? "true" : "false");
		break;
	case EZValueType::arrayValue:
		writeArrayValue(value);
		break;
	case EZValueType::objectValue: {
		auto members = value.memberNames();
		if (members.empty()) {
			pushValue("{}");
		}
		else {
			writeWithIndent("{");
			indent();
			auto it = members.begin();
			for (;;) {
				const std::string& name = *it;
				const auto& childValue = value[name];
				writeWithIndent(valueToQuotedString(name.c_str()));
				document_ += " : ";
				writeValue(childValue);
				if (++it == members.end()) {
					break;
				}
				document_ += ',';
			}
			unindent();
			writeWithIndent("}");
		}
	} 
	break;
	}
}

inline void EZStyledWriter::writeArrayValue(const EZValue& value) 
{
	size_t size = value.size();
	if (size == 0)
		pushValue("[]");
	else {
		bool isArrayMultiLine = isMultineArray(value);
		if (isArrayMultiLine) {
			writeWithIndent("[");
			indent();
			bool hasChildValue = !childValues_.empty();
			unsigned index = 0;
			for (;;) {
				const auto& childValue = value[index];
				if (hasChildValue)
					writeWithIndent(childValues_[index]);
				else {
					writeIndent();
					writeValue(childValue);
				}
				if (++index == size) {
					break;
				}
				document_ += ',';
			}
			unindent();
			writeWithIndent("]");
		}
		else // output on a single line
		{
			assert(childValues_.size() == size);
			document_ += "[ ";
			for (unsigned index = 0; index < size; ++index) {
				if (index > 0)
					document_ += ", ";
				document_ += childValues_[index];
			}
			document_ += " ]";
		}
	}
}

inline bool EZStyledWriter::isMultineArray(const EZValue& value)
{
	auto size = value.size();
	bool isMultiLine = size * 3 >= rightMargin_;
	childValues_.clear();
	for (auto index = 0; index < size && !isMultiLine; ++index) {
		const auto& childValue = value[index];
		isMultiLine = ((childValue.isArray() || childValue.isObject()) &&
			childValue.size() > 0);
	}
	if (!isMultiLine) // check if line length > max line length
	{
		childValues_.reserve(size);
		addChildValues_ = true;
		size_t lineLength = 4 + (size - 1) * 2; // '[ ' + ', '*n + ' ]'
		for (auto index = 0; index < size; ++index) {			
			writeValue(value[index]);
			lineLength += childValues_[index].length();
		}
		addChildValues_ = false;
		isMultiLine = isMultiLine || lineLength >= rightMargin_;
	}
	return isMultiLine;
}

inline void EZStyledWriter::pushValue(const std::string& value) 
{
	if (addChildValues_)
		childValues_.push_back(value);
	else
		document_ += value;
}

inline void EZStyledWriter::writeIndent() 
{
	if (!document_.empty()) {
		char last = document_[document_.length() - 1];
		if (last == ' ') // already indented
			return;
		if (last != '\n') // Comments may add new-line
			document_ += '\n';
	}
	document_ += indentString_;
}

inline void EZStyledWriter::writeWithIndent(const std::string& value)
{
	writeIndent();
	document_ += value;
}

inline void EZStyledWriter::indent() 
{
	indentString_ += std::string(indentSize_, ' '); 
}

inline void EZStyledWriter::unindent() 
{
	assert(indentString_.size() >= indentSize_);
	indentString_.resize(indentString_.size() - indentSize_);
}


inline std::string EZStyledWriter::valueToQuotedString(const char *value)
{
	// Not sure how to handle unicode...
	if (strpbrk(value, "\"\\\b\f\n\r\t") == NULL && !containsControlCharacter(value))
		return std::string("\"") + value + "\"";
	// We have to walk value and escape any special characters.
	// Appending to std::string is not efficient, but this should be rare.
	// (Note: forward slashes are *not* rare, but I am not escaping them.)
	size_t maxsize = strlen(value) * 2 + 3; // allescaped+quotes+NULL
	std::string result;
	result.reserve(maxsize); // to avoid lots of mallocs
	result += "\"";
	for (const char* c = value; *c != 0; ++c)
	{
		switch (*c)
		{
		case '\"':
			result += "\\\"";
			break;
		case '\\':
			result += "\\\\";
			break;
		case '\b':
			result += "\\b";
			break;
		case '\f':
			result += "\\f";
			break;
		case '\n':
			result += "\\n";
			break;
		case '\r':
			result += "\\r";
			break;
		case '\t':
			result += "\\t";
			break;
			//case '/':
			// Even though \/ is considered a legal escape in JSON, a bare
			// slash is also legal, so I see no reason to escape it.
			// (I hope I am not misunderstanding something.
			// blep notes: actually escaping \/ may be useful in javascript to avoid </ 
			// sequence.
			// Should add a flag to allow this compatibility mode and prevent this 
			// sequence from occurring.
		default:
			if (isControlCharacter(*c))
			{
				std::ostringstream oss;
				oss << "\\u" << std::hex << std::uppercase << std::setfill('0') << std::setw(4) << static_cast<int>(*c);
				result += oss.str();
			}
			else
			{
				result += *c;
			}
			break;
		}
	}
	result += "\"";
	return result;
}


inline bool EZStyledWriter::isControlCharacter(char ch) {
	return ch > 0 && ch <= 0x1F;
}

inline bool EZStyledWriter::containsControlCharacter(const char* str)
{
	while (*str)
	{
		if (isControlCharacter(*(str++))) {
			return true;
		}
	}
	return false;
}